import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import plotly.graph_objects as go
import numpy as np
from scipy import stats as st
import math as mth
import warnings
warnings.filterwarnings('ignore')
Для стартапа, продающего продукты питания нужно изучить поведение пользователей мобильного приложения.
Изучить воронку продаж. Узнайть, как пользователи доходят до покупки. Сколько пользователей доходит до покупки, а сколько — «застревает» на предыдущих шагах? На каких именно?
Исследовать результаты A/A/B-эксперимента. Дизайнеры захотели поменять шрифты во всём приложении, а менеджеры испугались, что пользователям будет непривычно. Договорились принять решение по результатам A/A/B-теста. Пользователей разбили на 3 группы: 2 контрольные со старыми шрифтами и одну экспериментальную — с новыми. Необходимо определить какой шрифт лучше.
Функция для обзора датафрейма
def df_overview(df):
print('\nИнформация о таблице')
df.info()
print('\nПервые строки таблицы')
display(df.head())
if df.duplicated().sum()== 0:
print('\nПолных дубликатов строк в таблице нет')
else:
print('\nКоличество полных дубликатов строк в таблице', df.duplicated().sum(), '\n')
for i in df.columns:
if df[i].isna().sum() == 0:
print('Пропущенных значений в столбце', i, ' нет')
else:
print('Количество пропусков в столбце', i, df[i].isna().sum())
print('\nПервичная статистика по таблице')
display(df.describe().T)
Посмотрим на таблицу для исследования.
logs_data = pd.read_csv('logs_exp.csv', sep='\t')
df_overview(logs_data)
Информация о таблице <class 'pandas.core.frame.DataFrame'> RangeIndex: 244126 entries, 0 to 244125 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 EventName 244126 non-null object 1 DeviceIDHash 244126 non-null int64 2 EventTimestamp 244126 non-null int64 3 ExpId 244126 non-null int64 dtypes: int64(3), object(1) memory usage: 7.5+ MB Первые строки таблицы
| EventName | DeviceIDHash | EventTimestamp | ExpId | |
|---|---|---|---|---|
| 0 | MainScreenAppear | 4575588528974610257 | 1564029816 | 246 |
| 1 | MainScreenAppear | 7416695313311560658 | 1564053102 | 246 |
| 2 | PaymentScreenSuccessful | 3518123091307005509 | 1564054127 | 248 |
| 3 | CartScreenAppear | 3518123091307005509 | 1564054127 | 248 |
| 4 | PaymentScreenSuccessful | 6217807653094995999 | 1564055322 | 248 |
Количество полных дубликатов строк в таблице 413 Пропущенных значений в столбце EventName нет Пропущенных значений в столбце DeviceIDHash нет Пропущенных значений в столбце EventTimestamp нет Пропущенных значений в столбце ExpId нет Первичная статистика по таблице
| count | mean | std | min | 25% | 50% | 75% | max | |
|---|---|---|---|---|---|---|---|---|
| DeviceIDHash | 244126.0 | 4.627568e+18 | 2.642425e+18 | 6.888747e+15 | 2.372212e+18 | 4.623192e+18 | 6.932517e+18 | 9.222603e+18 |
| EventTimestamp | 244126.0 | 1.564914e+09 | 1.771343e+05 | 1.564030e+09 | 1.564757e+09 | 1.564919e+09 | 1.565075e+09 | 1.565213e+09 |
| ExpId | 244126.0 | 2.470223e+02 | 8.244339e-01 | 2.460000e+02 | 2.460000e+02 | 2.470000e+02 | 2.480000e+02 | 2.480000e+02 |
Согласно документации таблица содержит следующую информацию:
EventName — название события;DeviceIDHash — уникальный идентификатор пользователя;EventTimestamp — время события;ExpId — номер эксперимента: 246 и 247 — контрольные группы, а 248 — экспериментальная.В данных обнаружены следующие проблемы:
Имеются дубликаты строк.
Названия столбцов не соответствуют "хорошему" стилю.
Столбец EventTimestamp содержащий дату, записан в формате int
Приведем названия столбцов к "хорошему стилю"
logs_data.columns = ['event_name', 'device_id_hash', 'event_time_stamp', 'exp_id']
logs_data.columns
Index(['event_name', 'device_id_hash', 'event_time_stamp', 'exp_id'], dtype='object')
Удалим полные дубликаты строк
print('Количество строк в таблице до удаления дубликатов', len(logs_data))
logs_data = logs_data.drop_duplicates().reset_index(drop=True)
print('Количество строк в таблице после удаления дубликатов', len(logs_data))
Количество строк в таблице до удаления дубликатов 244126 Количество строк в таблице после удаления дубликатов 243713
Преобразуем столбец, содержащий дату и время в формат datetime и выделим дату в отдельый столбец event_date
logs_data['event_time_stamp']=pd.to_datetime(logs_data['event_time_stamp'], unit='s')
logs_data['event_date'] = pd.to_datetime(logs_data['event_time_stamp'], format='%d-%m-%Y')
logs_data['event_date'] = logs_data['event_time_stamp'].dt.date
logs_data.info()
logs_data.head()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 243713 entries, 0 to 243712 Data columns (total 5 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 event_name 243713 non-null object 1 device_id_hash 243713 non-null int64 2 event_time_stamp 243713 non-null datetime64[ns] 3 exp_id 243713 non-null int64 4 event_date 243713 non-null object dtypes: datetime64[ns](1), int64(2), object(2) memory usage: 9.3+ MB
| event_name | device_id_hash | event_time_stamp | exp_id | event_date | |
|---|---|---|---|---|---|
| 0 | MainScreenAppear | 4575588528974610257 | 2019-07-25 04:43:36 | 246 | 2019-07-25 |
| 1 | MainScreenAppear | 7416695313311560658 | 2019-07-25 11:11:42 | 246 | 2019-07-25 |
| 2 | PaymentScreenSuccessful | 3518123091307005509 | 2019-07-25 11:28:47 | 248 | 2019-07-25 |
| 3 | CartScreenAppear | 3518123091307005509 | 2019-07-25 11:28:47 | 248 | 2019-07-25 |
| 4 | PaymentScreenSuccessful | 6217807653094995999 | 2019-07-25 11:48:42 | 248 | 2019-07-25 |
Дубликаты строк удалять не будем, поскольку для построения воронок их наличие не имеет значения.
print('Лог содержит', logs_data['event_name'].nunique(), 'уникальных событий:\n', logs_data['event_name'].unique())
print('\nВсего событий', logs_data['event_name'].count())
print('\nВ логе', logs_data['device_id_hash'].nunique(), 'пользователей')
Лог содержит 5 уникальных событий: ['MainScreenAppear' 'PaymentScreenSuccessful' 'CartScreenAppear' 'OffersScreenAppear' 'Tutorial'] Всего событий 243713 В логе 7551 пользователей
Посмотрим на статистику событий по пользователям.
(
logs_data.groupby('device_id_hash')
.agg({'event_name' : 'count'})
.describe().T
)
| count | mean | std | min | 25% | 50% | 75% | max | |
|---|---|---|---|---|---|---|---|---|
| event_name | 7551.0 | 32.275593 | 65.154219 | 1.0 | 9.0 | 20.0 | 37.0 | 2307.0 |
Один пользователь совершает от 1 до 2308 событий.
В среднем на одного пользователя приходится 32 события. Медианное количество событий на одного пользователя 20 . Как видно среднее значение сильно смещено в большую сторону из-за высокой активности отдельных пользователей, которых немного (75 перцентиль всего 37,5 а максисальное количество событий 2308).
Определение актуального периода данных.
print('самая ранняя дата лога', logs_data['event_date'].min())
print('самая поздняя дата лога', logs_data['event_date'].max())
самая ранняя дата лога 2019-07-25 самая поздняя дата лога 2019-08-07
Мы распологаем данными с 25 июля по 08 августа 2019 г. Посмотрим на распределение событий по датам.
(
logs_data.groupby('event_date')
.agg({'event_name': 'count'})
.plot(kind='bar', grid=True, figsize=(10, 5))
)
plt.title('Распределение событий лога по времени')
plt.legend(['Количество\nсобытий в день'])
plt.ylabel('Количество событий')
plt.xlabel('Дата')
plt.show()
Полными данными о событиях мы располагаем начиная с 01 августа. Более ранние события скорее всего попали в логи, потому что загрузились в систему 01 августа и позже, но отображаются по дате их совершения.
Посмотрим сколько событий и пользователей содержат логи до 01.08
not_relevant_logs = (
logs_data.query('event_time_stamp < 20190801')
.groupby('exp_id')
.agg({'event_name': 'count', 'device_id_hash': 'nunique'})
)
display(not_relevant_logs)
print('Всего событий в неактуальном периоде', not_relevant_logs['event_name'].sum(),
'\nЭто составляет',
round(not_relevant_logs['event_name'].sum() / logs_data['event_name'].count() * 100, 2),
'% всех событий.')
device_in_not_relevant = logs_data.query('event_time_stamp < 20190801')['device_id_hash'].unique()
print(
'Всего пользователей в неактуальном периоде',
len(device_in_not_relevant)
)
print(
'\nВсего пользователей в актуальном периоде',
logs_data.query('event_time_stamp >= 20190801')['device_id_hash'].nunique()
)
print(
'В актуальный и неактуальный период попали',
logs_data.query('event_time_stamp >= 20190801 and device_id_hash in @device_in_not_relevant')['device_id_hash'].nunique(),
'пользователей'
)
print(
'Будет исключено',
(len(device_in_not_relevant) -
logs_data.query('event_time_stamp >= 20190801 and device_id_hash in @device_in_not_relevant')
['device_id_hash'].nunique()),
'пользователей, попавших только в неактуальный период',
'\nчто составляет',
round((len(device_in_not_relevant) -
logs_data.query('event_time_stamp >= 20190801 and device_id_hash in @device_in_not_relevant')
['device_id_hash'].nunique()) / logs_data['device_id_hash'].nunique(),2),
'% пользователей'
)
| event_name | device_id_hash | |
|---|---|---|
| exp_id | ||
| 246 | 879 | 459 |
| 247 | 928 | 484 |
| 248 | 1019 | 508 |
Всего событий в неактуальном периоде 2826 Это составляет 1.16 % всех событий. Всего пользователей в неактуальном периоде 1451 Всего пользователей в актуальном периоде 7534 В актуальный и неактуальный период попали 1434 пользователей Будет исключено 17 пользователей, попавших только в неактуальный период что составляет 0.0 % пользователей
Отфильтруем таблицу по актуальному периоду
relevant_logs = logs_data.query('event_time_stamp >= 20190801')
relevant_logs.info()
<class 'pandas.core.frame.DataFrame'> Int64Index: 240887 entries, 2826 to 243712 Data columns (total 5 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 event_name 240887 non-null object 1 device_id_hash 240887 non-null int64 2 event_time_stamp 240887 non-null datetime64[ns] 3 exp_id 240887 non-null int64 4 event_date 240887 non-null object dtypes: datetime64[ns](1), int64(2), object(2) memory usage: 11.0+ MB
Проверим, что во всех группах остались пользователи
(
relevant_logs.groupby('exp_id')
.agg({'device_id_hash': 'nunique'})
)
| device_id_hash | |
|---|---|
| exp_id | |
| 246 | 2484 |
| 247 | 2513 |
| 248 | 2537 |
Пользователи есть во всех группах.
Проверим нет ли пересечения пользователей между группами
print(
'Данные содержат',
relevant_logs.groupby('device_id_hash')
.agg({'exp_id' : 'nunique'}).reset_index().query('exp_id >= 2').device_id_hash.nunique(),
'уникальных пользователей, попавших в несколько групп теста.\n'
)
Данные содержат 0 уникальных пользователей, попавших в несколько групп теста.
Посмотрим как часто встречается каждое событие, сколько уникальных пользователей его совершают.
funnel =(
relevant_logs.groupby('event_name')
.agg({'exp_id': 'count','device_id_hash': 'nunique'})
.sort_values(by='exp_id', ascending=False)
)
funnel.columns=['event','devices']
funnel['%_devices_in_event'] =(
round(funnel['devices'] / relevant_logs['device_id_hash'].nunique() * 100, 2)
)
funnel
| event | devices | %_devices_in_event | |
|---|---|---|---|
| event_name | |||
| MainScreenAppear | 117328 | 7419 | 98.47 |
| OffersScreenAppear | 46333 | 4593 | 60.96 |
| CartScreenAppear | 42303 | 3734 | 49.56 |
| PaymentScreenSuccessful | 33918 | 3539 | 46.97 |
| Tutorial | 1005 | 840 | 11.15 |
Не все события выстроились в последовательную цепочкую Событие Tutorial (скорее всего просмотр руководства по пользованию приложением) должно происходить после первого шага (MainScreenAppear), слишком мало пользователей делают этот шаг, Tutorial не нужно учитывать при подсчете воронки.
Кроме того, в первом шаге у нас только 98% всех уникальных пользователей. Возможно, с некоторых устройств пользователей не отсылаются определенные события.
Посчитаем конверсию каждого события относительно предыдущего и абсолютную конверсию (относительно общего числа пользователей)
devices_first_event = relevant_logs.query('event_name == "MainScreenAppear"')['device_id_hash']
#relevant_logs = relevant_logs.query('device_id_hash in @devices_first_event and event_name != "Tutorial"')
relevant_logs = relevant_logs.query('event_name != "Tutorial"')
funnel =(
relevant_logs
.groupby('event_name').agg({'device_id_hash':'nunique'})
.sort_values(by='device_id_hash', ascending=False)
)
funnel.columns=['devices']
funnel['prev_step_devices'] = funnel['devices'].shift()
funnel['conv'] =(
round(funnel['devices'] / relevant_logs['device_id_hash'].nunique(),
2)
)
funnel['conv_prev_step'] = (
round(funnel['devices']/funnel['prev_step_devices'], 2)
)
fig = go.Figure(go.Funnel(y=funnel.reset_index()['event_name'], x=funnel['devices']))
fig.update_layout(title='Воронка продаж приложения')
fig.show()
funnel
| devices | prev_step_devices | conv | conv_prev_step | |
|---|---|---|---|---|
| event_name | ||||
| MainScreenAppear | 7419 | NaN | 0.99 | NaN |
| OffersScreenAppear | 4593 | 7419.0 | 0.61 | 0.62 |
| CartScreenAppear | 3734 | 4593.0 | 0.50 | 0.81 |
| PaymentScreenSuccessful | 3539 | 3734.0 | 0.47 | 0.95 |
Вывод
Воронка продаж в приложении проходит в четыре этапа:
Конверсия в покупку у приложения 47%. Это очень высокий показатель. Хорошим показателем средней конверсии для интернет-магазинов этой ниши считается более 30%.https://www.shopolog.ru/metodichka/analytics/issledovanie-onlayn-rynok-produktov-pitaniya-za-1-polugodie-2021-goda/
Рассмотрим все этапы воронки.
Больше всего пользователей 38% теряется после первого шага. Возможно реклама привлекает нецелевую аудиторию приложения, причиной также может может быть не интуитивно понятный интерфейс.
До третьего этапа - корзины доходит 50% всех пользователей, здесь мы теряем 12%. В этом случае возможно проблема в отсутствии на странице предложения необходимой пользователям информации, или недостаточно привлекательном оформлении страницы.
От первого события до оплаты доходит 47% всех пользователей. Часть пользователей (3%) также теряется от этапа корзины до оплаты. Возможно они не находят удобного для них способа оплаты или доставки.
Посмотрим на группы по количеству пользователей
trials = relevant_logs.groupby('exp_id').agg({'device_id_hash': 'nunique'})
trials
| device_id_hash | |
|---|---|
| exp_id | |
| 246 | 2483 |
| 247 | 2512 |
| 248 | 2535 |
Максимальная разница в количестве пользователей групп 2,09% (между группой 248 и 246). Разинца между группами 247 и 246 (АА группы) 1,17%
Построим воронки для каждой группы, а также для объединенной А/А группы (246 и 247), посчитаем абсолютную конверсию на каждом шаге
funnel_by_exp = (
relevant_logs.pivot_table(index='event_name', columns='exp_id', values='device_id_hash', aggfunc='nunique')
.sort_values(by=246, ascending=False)
)
funnel_by_exp['246+247'] = funnel_by_exp[246] + funnel_by_exp[247]
funnel_by_exp['conv_246'] = round(funnel_by_exp[246] / trials.loc[246].values[0], 3)
funnel_by_exp['conv_247'] = round(funnel_by_exp[247] / trials.loc[247].values[0], 3)
funnel_by_exp['conv_248'] = round(funnel_by_exp[248] / trials.loc[248].values[0], 3)
funnel_by_exp['conv_246+247'] = round(funnel_by_exp['246+247'] / (trials.loc[246].values[0] +trials.loc[247].values[0]), 3)
(
funnel_by_exp[['conv_246','conv_247', 'conv_248', 'conv_246+247']]
.sort_values(by='conv_246')
.plot(kind='barh', figsize=(12, 7), grid=True)
)
plt.title('Абсолютные конверсии в событие\n для групп эксперимента')
plt.legend(title='Конверсия\nгруппы')
plt.ylabel('Событие')
plt.xlabel('Доля пользователей группы, совершивших событие')
#plt.tight_layout()
plt.show()
funnel_by_exp
| exp_id | 246 | 247 | 248 | 246+247 | conv_246 | conv_247 | conv_248 | conv_246+247 |
|---|---|---|---|---|---|---|---|---|
| event_name | ||||||||
| MainScreenAppear | 2450 | 2476 | 2493 | 4926 | 0.987 | 0.986 | 0.983 | 0.986 |
| OffersScreenAppear | 1542 | 1520 | 1531 | 3062 | 0.621 | 0.605 | 0.604 | 0.613 |
| CartScreenAppear | 1266 | 1238 | 1230 | 2504 | 0.510 | 0.493 | 0.485 | 0.501 |
| PaymentScreenSuccessful | 1200 | 1158 | 1181 | 2358 | 0.483 | 0.461 | 0.466 | 0.472 |
Самые высокие показатели конверсии в продажи и по каждому событию у группы 246. Самая низкая конверсия в продажи у группы 247. У группы 248 на первых трех шагах конверсия несколько ниже, чем у 246 и 247, однако конверсия в последнее событие у 248 группы выше чему 247.
Проверим статистическими методами есть ли различия между этими выборками.
Две контрольные группы эксперимента А/А - 246 и 247. Нам нужно проверить есть ли статистически значимая разница в конверсиях в каждый шаг между эти группами. Гипотеза о равенстве долей двух генеральных совокупностей проверяется z-тестом.
Напишем функцию для проверки равенства долей на каждом шаге z-тестом.
def check_hypothesise(successes1, successes2, trials1, trials2):
p1 = successes1 / trials1
p2 = successes2 / trials2
p_combined = (successes1 + successes2) / (trials1 + trials2)
difference = p1 - p2
z_value = difference / mth.sqrt(p_combined * (1 - p_combined) * (1/trials1 + 1/trials2))
distr = st.norm(0, 1)
p_value = (1 - distr.cdf(abs(z_value))) * 2
return p_value
Уровень значимости alpha для сравнения двух А/А групп 0,01
При сравнении групп мы проверям 16 статистических гипотез о равенстве долей по каждому событию (по 4 гипотезы для каждого сочетания групп). Скорректируем уровень значимости на число сравнений при помощи метода Шидока.
alpha = 1 - (1 - 0.01)**(1/16)
print('Уровень значимости для проверки равенства долей групп 246 и 247 равен', round(alpha, 3))
Уровень значимости для проверки равенства долей групп 246 и 247 равен 0.001
Нулевая гипотеза - Конверсии в событие двух генеральных совокупностей выборок 246 и 247 равны.
Альтернативная гипотеза - Конверсии в событие двух генеральных совокупностей выборок 246 и 247 различаются.
for event in funnel_by_exp.index:
p_value = check_hypothesise(funnel_by_exp.loc[event,246],
funnel_by_exp.loc[event,247],
trials.loc[246].values[0],
trials.loc[247].values[0])
if p_value < alpha:
print(
f'Для события {event}:',
f'Отвергаем нулевую гипотезу: \nмежду долями в группах 246 и 247 по этому событию есть значимая разница\n',
'p_value=', round(p_value, 3)
)
else:
print(
f'Для события {event}:',
f'\nНе получилось отвергнуть нулевую гипотезу,\nнет оснований считать доли групп 246 и 247 по этому событию разными\n',
'p_value=', round(p_value, 3),'\n'
)
Для события MainScreenAppear: Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли групп 246 и 247 по этому событию разными p_value= 0.753 Для события OffersScreenAppear: Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли групп 246 и 247 по этому событию разными p_value= 0.248 Для события CartScreenAppear: Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли групп 246 и 247 по этому событию разными p_value= 0.229 Для события PaymentScreenSuccessful: Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли групп 246 и 247 по этому событию разными p_value= 0.114
Вывод
Контрольные группы 246 и 247 сформированы успешно, вероятность того, что между ними нет статистически значимых различий выше уровня значимости.
Уровень значимости alpha для проверки равен 0,05.
Скорректируем его с учетом множественных сравнений, применив метод Шидака (мы проверяем 16 статистических гипотез).
alpha = 1 - (1 - 0.05)**(1/16)
print('Уровень значимости для проверки равенства долей 248 группы и контрольных групп 246, 247, 246+247 равен', round(alpha, 3))
Уровень значимости для проверки равенства долей 248 группы и контрольных групп 246, 247, 246+247 равен 0.003
Сравним группы 248 и 246
Нулевая гипотеза - Конверсии в событие двух генеральных совокупностей выборок 246 и 248 равны.
Альтернативная гипотеза - Конверсии в событие двух генеральных совокупностей выборок 246 и 248 различаются.
for event in funnel_by_exp.index:
p_value = check_hypothesise(funnel_by_exp.loc[event,246],
funnel_by_exp.loc[event,248],
trials.loc[246].values[0],
trials.loc[248].values[0])
if p_value < alpha:
print(
f'Для события {event}:',
f'Отвергаем нулевую гипотезу: \nмежду долями в группах 246 и 248 по этому событию есть значимая разница\n',
'p_value=', round(p_value, 3)
)
else:
print(
f'Для события {event}:',
f'\nНе получилось отвергнуть нулевую гипотезу,\nнет оснований считать доли групп 246 и 248 по этому событию разными\n',
'p_value=', round(p_value, 3),'\n'
)
Для события MainScreenAppear: Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли групп 246 и 248 по этому событию разными p_value= 0.339 Для события OffersScreenAppear: Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли групп 246 и 248 по этому событию разными p_value= 0.214 Для события CartScreenAppear: Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли групп 246 и 248 по этому событию разными p_value= 0.081 Для события PaymentScreenSuccessful: Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли групп 246 и 248 по этому событию разными p_value= 0.217
Вывод
Вероятность того, что между группами 248 и 246 нет статистически значимых различий в конверсиях по всем событиям, выше уровня значимости.
Сравним группы 248 и 247
Нулевая гипотеза - Конверсии в событие двух генеральных совокупностей выборок 247 и 248 равны.
Альтернативная гипотеза - Конверсии в событие двух генеральных совокупностей выборок 247 и 248 различаются.
for event in funnel_by_exp.index:
p_value = check_hypothesise(funnel_by_exp.loc[event,247],
funnel_by_exp.loc[event,248],
trials.loc[247].values[0],
trials.loc[248].values[0])
if p_value < alpha:
print(
f'Для события {event}:',
f'Отвергаем нулевую гипотезу: \nмежду долями в группах 247 и 248 по этому событию есть значимая разница\n',
'p_value=', round(p_value, 3)
)
else:
print(
f'Для события {event}:',
f'\nНе получилось отвергнуть нулевую гипотезу,\nнет оснований считать доли групп 247 и 248 по этому событию разными\n',
'p_value=', round(p_value, 3),'\n'
)
Для события MainScreenAppear: Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли групп 247 и 248 по этому событию разными p_value= 0.519 Для события OffersScreenAppear: Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли групп 247 и 248 по этому событию разными p_value= 0.933 Для события CartScreenAppear: Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли групп 247 и 248 по этому событию разными p_value= 0.588 Для события PaymentScreenSuccessful: Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли групп 247 и 248 по этому событию разными p_value= 0.728
Вывод
Вероятность того, что между группами 248 и 247 нет статистически значимых различий в конверсиях по всем событиям, выше уровня значимости.
Сравним группы 248 и 246+247
Нулевая гипотеза - Конверсии в событие двух генеральных совокупностей выборок 246+247 и 248 равны.
Альтернативная гипотеза - Конверсии в событие двух генеральных совокупностей выборок 246+247 и 248 различаются.
for event in funnel_by_exp.index:
p_value = check_hypothesise(funnel_by_exp.loc[event,'246+247'],
funnel_by_exp.loc[event,248],
(trials.loc[246].values[0]+trials.loc[247].values[0]),
trials.loc[248].values[0])
if p_value < alpha:
print(
f'Для события {event}:',
f'Отвергаем нулевую гипотезу: \nмежду долями в группах 246+247 и 248 по этому событию есть значимая разница\n',
'p_value=', round(p_value, 3)
)
else:
print(
f'Для события {event}:',
f'\nНе получилось отвергнуть нулевую гипотезу,\nнет оснований считать доли групп 246+247 и 248 по этому событию разными\n',
'p_value=', round(p_value, 3),'\n'
)
Для события MainScreenAppear: Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли групп 246+247 и 248 по этому событию разными p_value= 0.349 Для события OffersScreenAppear: Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли групп 246+247 и 248 по этому событию разными p_value= 0.446 Для события CartScreenAppear: Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли групп 246+247 и 248 по этому событию разными p_value= 0.187 Для события PaymentScreenSuccessful: Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли групп 246+247 и 248 по этому событию разными p_value= 0.611
Вывод
Вероятность того, что между группами 248 и 246+247 нет статистически значимых различий в конверсиях по всем событиям, выше уровня значимости.
В целом конверсия на каждом шаге у группы с измененными шрифтами в приложении 248 была несколько ниже, чем у контрольной 246+247. Однако, проверка статистическими методами показала, что вероятнее всего эти различия случайны.
Таким образом можно сказать, что применение нового шрифта в приложении не изменило поведения пользователей в отношении конверсии в проверяемые нами события.
В ходе исследования мы выяснили:
Можно сделать опрос, почему пользователи отказываются от покупки в последний момент. Для проверки удобства интерфейса нескольким тестовым пользователям можно дать задание пройти все этапы от поиска товара до оплаты.
Кроме того практически все пользователи переходят в оплату из корзины. Если у приложения есть функция быстрой покупки (со страницы предожения), то ей почти не пользуются. Следует обратить на это внимание.
Проверка результатов эксперимента с изменением шрифтов приложения показала следующее:
Изменение шрифтов не оказало влияния на поведение пользователей в части конверсии в шаги воронки продаж. В этом отношении нельзя сказать что новый шрифт лучше, чем старый или наоборот.